<!DOCTYPE html>
<html lang="en">
<head>
  <title>three.js webgl - physically based shading</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <link type="text/css" rel="stylesheet" href="main.css">
</head>

<body>
<div id="info">
  <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgl physically based shading testbed
</div>

<script type="module">

  import * as THREE from '../build/three.module.js';

  import Stats from './jsm/libs/stats.module.js';

  import { TrackballControls } from './jsm/controls/TrackballControls.js';

  let RC = [
    [[], [], []],
    [[], [], []],
    [[], [], []]
  ]

  let stats;

  let camera, scene, renderer;

  let mesh;

  let control;

  let sunLight, ambientLight;

  let shadowCameraHelper;

  let mouse,
    raycaster,
    direction,
    lastCheckedObj,
    firstCheckedObj,
    rotationVal = 0,
    rotationTotalVal = 0,
    rotationObjList = [],
    SRIndex = [],
    face

  const shadowConfig = {
    shadowCameraVisible: false,
    shadowCameraNear: 750,
    shadowCameraFar: 4000,
    shadowBias: -0.0002
  }

  const scale = [-75, 0, 75]

  const directionMap = {
    L: 'L', // 左
    R: 'R', // 右
    U: 'U', // 上
    D: 'D', // 下
    UZ: 'UZ', // z轴上
    DZ: 'DZ' // z轴下
  }
  const rotateDirectionMap = {
    clockwise: [directionMap.U, directionMap.R, directionMap.DZ],
    anticlockwise: [directionMap.L, directionMap.D, directionMap.UZ]
  }

  init();
  animate();

  function init () {
    const container = document.createElement('div');
    document.body.appendChild(container);

    // CAMERA
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 2, 10000);
    camera.position.set(500, 400 , 1000);
    camera.lookAt( 0, 0, 0 );

    // SCENE
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xfffffff);

    scene.add(new THREE.AxisHelper(2000));

    raycaster = new THREE.Raycaster();
    mouse = new THREE.Vector2();

    generateSC()

    function generateSC () {
      const materialLambert = new THREE.MeshPhongMaterial({
        shininess: 50,
        color: 0x000000,
        flatShading: true
      });
      const cubeGeometry = new THREE.BoxGeometry(75, 75, 75);
      const cornerGeometry = new THREE.BoxGeometry(1, 1, 1);
      const corner = []

      const roundedRectShape = new THREE.Shape();
      (function roundedRect (ctx, x, y, z, width, height, radius) {

        ctx.moveTo(x, y + radius);
        ctx.lineTo(x, y + height - radius);
        ctx.quadraticCurveTo(x, y + height, x + radius, y + height);
        ctx.lineTo(x + width - radius, y + height);
        ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
        ctx.lineTo(x + width, y + radius);
        ctx.quadraticCurveTo(x + width, y, x + width - radius, y);
        ctx.lineTo(x + radius, y);
        ctx.quadraticCurveTo(x, y, x, y + radius);
      })(roundedRectShape, -35.5, -35.5, 0, 71, 71, 10);

      for (let x = 0; x < scale.length; x++) {
        for (let y = 0; y < scale.length; y++) {
          for (let z = 0; z < scale.length; z++) {
            corner.push(addObject(cornerGeometry, materialLambert, scale[x], scale[y], scale[z]))
          }
        }
      }

      for (let x = 0; x < scale.length; x++) {
        for (let y = 0; y < scale.length; y++) {
          for (let z = 0; z < scale.length; z++) {
            RC[x][y][z] = new THREE.Group()
            RC[x][y][z].index = [x, y, z]
            RC[x][y][z].originalIndex = [x, y, z]
            scene.add(RC[x][y][z])

            RC[x][y][z].add(...corner)
            RC[x][y][z].add(addObject(cubeGeometry, materialLambert, scale[x], scale[y], scale[z], 0))
            if (x === 0) {
              RC[x][y][z].add(addShape(roundedRectShape, 0x3D81F6, scale[x] - 38, scale[y], scale[z], 0, Math.PI / 2, 0, 1))
            }
            if (x === 2) {
              RC[x][y][z].add(addShape(roundedRectShape, 0x009d54, scale[x] + 38, scale[y], scale[z], 0, Math.PI / 2, 0, 2))
            }
            if (y === 0) {
              RC[x][y][z].add(addShape(roundedRectShape, 0xdc422f, scale[x], scale[y] - 38, scale[z], Math.PI / 2, 0, 0, 3))
            }
            if (y === 2) {
              RC[x][y][z].add(addShape(roundedRectShape, 0xfdcc09, scale[x], scale[y] + 38, scale[z], Math.PI / 2, 0, 0, 4))
            }
            if (z === 0) {
              RC[x][y][z].add(addShape(roundedRectShape, 0xff6c00, scale[x], scale[y], scale[z] - 38, 0, 0, 0, 5))
            }
            if (z === 2) {
              RC[x][y][z].add(addShape(roundedRectShape, 0xffffff, scale[x], scale[y], scale[z] + 38, 0, 0, 0, 6))
            }
          }
        }
      }
    }

    function addShape (shape, color, x, y, z, rx, ry, rz, face) {
      let geometry = new THREE.ShapeGeometry(shape)
      let mesh = new THREE.Mesh(
        geometry,
        new THREE.MeshPhongMaterial({
          color: color,
          side: THREE.DoubleSide
        }));
      mesh.position.set(x, y, z)
      mesh.rotation.set(rx, ry, rz)
      mesh.face = face
      return mesh
    }

    function addObject (geometry, material, x, y, z, ry) {
      const tmpMesh = new THREE.Mesh(geometry, material);
      tmpMesh.material.color.offsetHSL(0.1, -0.1, 0);
      tmpMesh.position.set(x, y, z);
      tmpMesh.rotation.y = ry;
      tmpMesh.castShadow = true;
      tmpMesh.receiveShadow = true;
      return tmpMesh;
    }

    // LIGHTS
    ambientLight = new THREE.AmbientLight(0xfffffff);
    scene.add(ambientLight);
    sunLight = new THREE.DirectionalLight(0xffffff, 0.3);
    sunLight.position.set(1000, 2000, 1000);
    sunLight.castShadow = true;
    sunLight.shadow.camera.top = 750;
    sunLight.shadow.camera.bottom = -750;
    sunLight.shadow.camera.left = -750;
    sunLight.shadow.camera.right = 750;
    sunLight.shadow.camera.near = shadowConfig.shadowCameraNear;
    sunLight.shadow.camera.far = shadowConfig.shadowCameraFar;
    sunLight.shadow.mapSize.set(1024, 1024);
    sunLight.shadow.bias = shadowConfig.shadowBias;
    scene.add(sunLight);

    // SHADOW CAMERA HELPER

    shadowCameraHelper = new THREE.CameraHelper(sunLight.shadow.camera);
    shadowCameraHelper.visible = shadowConfig.shadowCameraVisible;
    scene.add(shadowCameraHelper);

    // RENDERER

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    //

    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.outputEncoding = THREE.sRGBEncoding;

    //
    control = new TrackballControls(camera, renderer.domElement);
    control.target.set(0, 120, 0);
    control.rotateSpeed = 1.0;
    control.zoomSpeed = 1.2;
    control.panSpeed = 0.8;

    control.staticMoving = true;

    control.keys = ['KeyA', 'KeyS', 'KeyD'];


    // STATS

    stats = new Stats();
    container.appendChild(stats.dom);

    // EVENTS
    window.addEventListener('resize', onWindowResize);
    
    window.addEventListener('pointerdown', pointerdown, true);
    window.addEventListener('pointerup', pointerup, false);
  }

  //

  function onWindowResize () {

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);

    control.handleResize();

  }

  function pointerup (event) {
    if (!control.enabled && face && !rotationVal) {
      event.preventDefault();
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
      raycaster.setFromCamera(mouse, camera)
      let intersects = raycaster.intersectObjects(scene.children, true)
      if (intersects.length > 1) {
        lastCheckedObj = intersects[0].object
        if (lastCheckedObj.id === firstCheckedObj.id) {
          return
        }
        direction = getDirection(lastCheckedObj.face)
        getObjList()
      }
    }
  }

  function pointerdown (event) {
    event.preventDefault();
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    let intersects = raycaster.intersectObjects(scene.children, true)
    if (intersects.length > 1) {
      firstCheckedObj = intersects[0].object
      face = intersects[0].object.face
      control.enabled = false
    } else {
      control.enabled = true
    }
  }

  //

  function animate () {
    requestAnimationFrame(animate)
    stats.begin();
    render();
    stats.end();

  }

  function render () {
    if (rotationTotalVal > 0) {
      rotationVal = Math.PI / 90
      rotationTotalVal -= rotationVal
    } else if (rotationVal) {
      const matrix = [[], [], []]
      for (let i = 0; i < rotationObjList.length; i++) {
        let index = rotationObjList[i].index
        matrix[index[SRIndex[0]]][index[SRIndex[1]]] = rotationObjList[i]
      }
      modelRotate(matrix)
      for (let i = 0; i < matrix.length; i++) {
        for (let n = 0; n < matrix[i].length; n++) {
          let index = matrix[i][n].index
          scene.add(matrix[i][n])
          RC[index[0]][index[1]][index[2]] = matrix[i][n]
        }
      }
      rotationVal = 0
    }


    if (rotationVal) {
      let vector3
      let angle
      for (let i = 0; i < rotationObjList.length; i++) {
        switch (direction) {
          case directionMap.DZ: {
            vector3 = new THREE.Vector3(0, 0, 1)
            angle = -rotationVal
            break
          }
          case directionMap.UZ: {
            vector3 = new THREE.Vector3(0, 0, 1)
            angle = rotationVal
            break
          }
          case directionMap.L: {
            vector3 = new THREE.Vector3(0, 1, 0)
            angle = -rotationVal
            break
          }
          case directionMap.R: {
            vector3 = new THREE.Vector3(0, 1, 0)
            angle = rotationVal
            break
          }
          case directionMap.U: {
            vector3 = new THREE.Vector3(1, 0, 0)
            angle = -rotationVal
            break
          }
          case directionMap.D: {
            vector3 = new THREE.Vector3(1, 0, 0)
            angle = rotationVal
          }
        }
        let quaternion = new THREE.Quaternion()
        quaternion.setFromAxisAngle(vector3, angle);
        rotationObjList[i].quaternion.premultiply(quaternion)
      }
    }


    // update
    control.update()
    renderer.render(scene, camera)
  }

  function modelRotate (matrix) {
    if (!matrix[0].length) {
      return
    }
    let tR = 0;
    let tC = 0;
    let dR = matrix.length - 1;
    let dC = matrix[0].length - 1;
    while (tR < dR) {
      rotateEdge(
        matrix,
        tR++,
        tC++,
        dR--,
        dC--,
        matrix[1][1].children[1] ? matrix[1][1].children[1].face : null
      )
    }
    return matrix
  }

  function rotateEdge (matrix, tR, tC, dR, dC, rotateFace) {
    let times = dC - tC
    let temp = 0
    let temp2
    if (rotateDirectionMap.clockwise.includes(direction)) {
      for (let i = 0; i !== times; i++) {
        temp = matrix[tR][tC + i]
        matrix[tR][tC + i] = matrix[dR - i][tC]
        matrix[dR - i][tC] = matrix[dR][dC - i]
        matrix[dR][dC - i] = matrix[tR + i][dC]
        matrix[tR + i][dC] = temp

        temp2 = {
          index: matrix[tR + i][dC].index,
          children: []
        }
        matrix[tR + i][dC].children.map(item => {
          temp2.children.push({ ...item })
        })

        matrix[tR + i][dC] = faceReplace(matrix[tR + i][dC], matrix[dR][dC - i], rotateFace)
        matrix[dR][dC - i] = faceReplace(matrix[dR][dC - i], matrix[dR - i][tC], rotateFace)
        matrix[dR - i][tC] = faceReplace(matrix[dR - i][tC], matrix[tR][tC + i], rotateFace)
        matrix[tR][tC + i] = faceReplace(matrix[tR][tC + i], temp2, rotateFace)

      }
    } else {
      for (let i = 0; i !== times; i++) {
        temp = matrix[tR][tC + i]
        matrix[tR][tC + i] = matrix[tR + i][dC]
        matrix[tR + i][dC] = matrix[dR][dC - i]
        matrix[dR][dC - i] = matrix[dR - i][tC]
        matrix[dR - i][tC] = temp

        temp2 = {
          index: matrix[dR - i][tC].index,
          children: []
        }
        matrix[dR - i][tC].children.map(item => {
          temp2.children.push({ ...item })
        })

        matrix[dR - i][tC] = faceReplace(matrix[dR - i][tC], matrix[dR][dC - i], rotateFace)
        matrix[dR][dC - i] = faceReplace(matrix[dR][dC - i], matrix[tR + i][dC], rotateFace)
        matrix[tR + i][dC] = faceReplace(matrix[tR + i][dC], matrix[tR][tC + i], rotateFace)
        matrix[tR][tC + i] = faceReplace(matrix[tR][tC + i], temp2, rotateFace)
      }
    }
  }

  /**
   * 面旋转
   * @param obj 被替换的
   * @param obj2 替换的
   * @param rotateFace
   * @returns {*}
   */
  function faceReplace (obj, obj2, rotateFace) {
    let faceArr = []
    let faceArr2 = []
    let objArr = []

    obj.children.map((item, index) => {
      if (item.face && item.face !== rotateFace) {
        faceArr.push(item.face)
        objArr.push({
          face: item.face,
          index: index
        })
      }
    })

    obj2.children.map(item => {
      if (item.face && item.face !== rotateFace) {
        faceArr2.push(item.face)
      }
    })

    if (faceArr2.length === 2) {
      let different = faceArr2.filter(v => !faceArr.includes(v))[0]
      let same = faceArr2.filter(v => faceArr.includes(v))[0]
      if (faceArr[0] !== different && faceArr[0] !== same) {
        objArr[0].face = same
        objArr[1].face = different
      } else {
        objArr[0].face = different
        objArr[1].face = same
      }
      obj.children[objArr[0].index].face = objArr[0].face
      obj.children[objArr[1].index].face = objArr[1].face
    } else {
      obj.children[objArr[0].index].face = faceArr2[0]
    }
    obj.index = obj2.index
    return obj
  }

  /**
   * 获取转动方向
   * @param lastFace
   */
  function getDirection (lastFace) {
    console.info(face, lastFace)
    let [lX, lY, lZ] = lastCheckedObj.parent.index
    let [fX, fY, fZ] = firstCheckedObj.parent.index
    
    if (lastFace === face) {
      switch (face) {
        case 1: {
          if (lZ === fZ) {
            return lY > fY ? directionMap.DZ : directionMap.UZ
          } else if (lY === fY) {
            return lZ > fZ ? directionMap.R : directionMap.L
          }
          break
        }
        case 2: {
          if (lZ === fZ) {
            return lY > fY ? directionMap.UZ : directionMap.DZ
          } else if (lY === fY) {
            return lZ > fZ ? directionMap.L : directionMap.R
          }
          break
        }
        case 3: {
          if (lZ === fZ) {
            return lX > fX ? directionMap.UZ : directionMap.DZ
          } else if (lX === fX) {
            return lZ > fZ ? directionMap.U : directionMap.D
          }
          break
        }
        case 4: {
          if (lZ === fZ) {
            return lX > fX ? directionMap.DZ : directionMap.UZ
          } else if (lX === fX) {
            return lZ > fZ ? directionMap.D : directionMap.U
          }
          break
        }
        case 5: {
          if (lY === fY) {
            return lX > fX ? directionMap.L : directionMap.R
          } else if (lX === fX) {
            return lY > fY ? directionMap.D : directionMap.U
          }
          break
        }
        case 6: {
          if (lY === fY) {
            return lX > fX ? directionMap.R : directionMap.L
          } else if (lX === fX) {
            return (lY > fY ? directionMap.U : directionMap.D)
          }
        }
      }
    } else {
      if (
        face === 6 && lastFace === 2 ||
        face === 2 && lastFace === 5 ||
        face === 1 && lastFace === 6 ||
        face === 5 && lastFace === 1
      ) {
        return directionMap.R
      } else if (
        face === 6 && lastFace === 4 ||
        face === 4 && lastFace === 5 ||
        face === 3 && lastFace === 6 ||
        face === 5 && lastFace === 3
      ) {
        return directionMap.U
      } else if (
        face === 6 && lastFace === 3 ||
        face === 4 && lastFace === 6 ||
        face === 3 && lastFace === 5 ||
        face === 5 && lastFace === 4
      ) {
        return directionMap.D
      } else if (
        face === 6 && lastFace === 1 ||
        face === 2 && lastFace === 6 ||
        face === 1 && lastFace === 5 ||
        face === 5 && lastFace === 2
      ) {
        return directionMap.L
      } else if (
        face === 2 && lastFace === 4 ||
        face === 4 && lastFace === 1 ||
        face === 1 && lastFace === 3 ||
        face === 3 && lastFace === 2
      ) {
        return directionMap.UZ
      } else if (
        face === 2 && lastFace === 3 ||
        face === 4 && lastFace === 2 ||
        face === 1 && lastFace === 4 ||
        face === 3 && lastFace === 1
      ) {
        return directionMap.DZ
      }
      
    }
    return null
  }

  /**
   * 获取需转动的块
   */
  function getObjList () {
    if (!direction) {
      return
    }
    
    rotationObjList = []
    let index = firstCheckedObj.parent.index
    if ([directionMap.DZ, directionMap.UZ].includes(direction)) {
      for (let i = 0; i < 3; i++) {
        for (let n = 0; n < 3; n++) {
          rotationObjList.push(RC[i][n][index[2]])
          SRIndex = [0, 1]
        }
      }
    } else if ([directionMap.L, directionMap.R].includes(direction)) {
      for (let i = 0; i < 3; i++) {
        for (let n = 0; n < 3; n++) {
          rotationObjList.push(RC[i][index[1]][n])
          SRIndex = [0, 2]
        }
      }
    } else if ([directionMap.D, directionMap.U].includes(direction)) {
      for (let i = 0; i < 3; i++) {
        for (let n = 0; n < 3; n++) {
          rotationObjList.push(RC[index[0]][i][n])
          SRIndex = [1, 2]
        }
      }
    }

    rotationTotalVal = Math.PI / 2
  }
</script>

</body>
</html>
